package com.bugsnag.ohos;

import ohos.app.Context;

import com.bugsnag.ohos.annotation.NonNull;
import com.bugsnag.ohos.annotation.Nullable;
import com.bugsnag.ohos.annotation.VisibleForTesting;

import java.io.File;
import java.util.Map;
import java.util.Set;

/**
 * User-specified configuration storage object, contains information
 * specified at the client level, api-key and endpoint configuration.
 */
@SuppressWarnings("ConstantConditions") // suppress warning about making redundant null checks
public class Configuration implements CallbackAware, MetadataAware, UserAware {
    private static final int MIN_BREADCRUMBS = 0;
    private static final int MAX_BREADCRUMBS = 100;
    private static final int VALID_API_KEY_LEN = 32;
    private static final long MIN_LAUNCH_CRASH_THRESHOLD_MS = 0L;
    final ConfigInternal impl;
    private boolean launchDurationMillis;

    /**
     * Constructs a new Configuration object with default values.
     *
     * @param apiKey
     */
    public Configuration(@NonNull String apiKey) {
        validateApiKey(apiKey);
        impl = new ConfigInternal(apiKey);
    }

    /**
     * Loads a Configuration object from values supplied as meta-data elements in your
     * config.
     *
     * @param context
     * @return Configuration
     */
    @NonNull
    public static Configuration load(@NonNull Context context) {
        return ConfigInternal.load(context);
    }

    /**
     * load Configuration
     *
     * @param context
     * @param apiKey
     * @return Configuration
     */
    @NonNull
    static Configuration load(@NonNull Context context, @NonNull String apiKey) {
        return ConfigInternal.load(context, apiKey);
    }

    private void validateApiKey(String value) {
        if (isInvalidApiKey(value)) {
            DebugLogger.INSTANCE.w("Invalid configuration. "
                + "apiKey should be a 32-character hexademical string, got " + value);
        }
    }

    @VisibleForTesting
    static boolean isInvalidApiKey(String apiKey) {
        if (Intrinsics.isEmpty(apiKey)) {
            throw new IllegalArgumentException("No Bugsnag API Key set");
        }
        if (apiKey.length() != VALID_API_KEY_LEN) {
            return true;
        }
        for (int k = 0; k < VALID_API_KEY_LEN; k++) {
            char chr = apiKey.charAt(k);
            if (!Character.isDigit(chr) && (chr < 'a' || chr > 'f')) {
                return true;
            }
        }
        return false;
    }

    private void logNull(String property) {
        getLogger().e("Invalid null value supplied to config." + property + ", ignoring");
    }

    /**
     * Retrieves the API key used for events sent to Bugsnag.
     *
     * @return api key
     */
    @NonNull
    public String getApiKey() {
        return impl.getApiKey();
    }

    /**
     * Changes the API key used for events sent to Bugsnag.
     *
     * @param apiKey
     */
    public void setApiKey(@NonNull String apiKey) {
        validateApiKey(apiKey);
        impl.setApiKey(apiKey);
    }

    public boolean isLaunchDurationMillis() {
        return launchDurationMillis;
    }

    public void setLaunchDurationMillis(boolean launchDurationMillis) {
        this.launchDurationMillis = launchDurationMillis;
    }

    /**
     * Set the application version sent to Bugsnag. We'll automatically pull your app version
     * from the versionName field in your config.json file.
     *
     * @return app version
     */
    @Nullable
    public String getAppVersion() {
        return impl.getAppVersion();
    }

    /**
     * Set the application version sent to Bugsnag. We'll automatically pull your app version
     * from the versionName field in your config.json file.
     *
     * @param appVersion
     */
    public void setAppVersion(@Nullable String appVersion) {
        impl.setAppVersion(appVersion);
    }

    /**
     * We'll automatically pull your versionCode from the versionCode field
     * in your config.json file. If you'd like to override this you
     * can set this property.
     *
     * @return version code
     */
    @Nullable
    public int getVersionCode() {
        return impl.getVersionCode();
    }

    /**
     * We'll automatically pull your versionCode from the versionCode field
     * in your config.json file. If you'd like to override this you
     * can set this property.
     *
     * @param versionCode
     */
    public void setVersionCode(@Nullable int versionCode) {
        impl.setVersionCode(versionCode);
    }

    /**
     * If you would like to distinguish between errors that happen in different stages of the
     * application release process (development, production, etc) you can set the releaseStage
     * that is reported to Bugsnag.
     * <p>
     * If you are running a debug build, we'll automatically set this to "development",
     * otherwise it is set to "production". You can control whether events are sent for
     * specific release stages using the enabledReleaseStages option.
     *
     * @return release stage
     */
    @Nullable
    public String getReleaseStage() {
        return impl.getReleaseStage();
    }

    /**
     * If you would like to distinguish between errors that happen in different stages of the
     * application release process (development, production, etc) you can set the releaseStage
     * that is reported to Bugsnag.
     * <p>
     * If you are running a debug build, we'll automatically set this to "development",
     * otherwise it is set to "production". You can control whether events are sent for
     * specific release stages using the enabledReleaseStages option.
     *
     * @param releaseStage
     */
    public void setReleaseStage(@Nullable String releaseStage) {
        impl.setReleaseStage(releaseStage);
    }

    /**
     * Controls whether we should capture and serialize the state of all threads at the time
     * of an error.
     * <p>
     * By default sendThreads is set to Thread.ThreadSendPolicy.ALWAYS. This can be set to
     * Thread.ThreadSendPolicy.NEVER to disable or Thread.ThreadSendPolicy.UNHANDLED_ONLY
     * to only do so for unhandled errors.
     *
     * @return ThreadSendPolicy
     */
    @NonNull
    public ThreadSendPolicy getSendThreads() {
        return impl.getSendThreads();
    }

    /**
     * Controls whether we should capture and serialize the state of all threads at the time
     * of an error.
     * <p>
     * By default sendThreads is set to Thread.ThreadSendPolicy.ALWAYS. This can be set to
     * Thread.ThreadSendPolicy.NEVER to disable or Thread.ThreadSendPolicy.UNHANDLED_ONLY
     * to only do so for unhandled errors.
     *
     * @param sendThreads
     */
    public void setSendThreads(@NonNull ThreadSendPolicy sendThreads) {
        if (sendThreads != null) {
            impl.setSendThreads(sendThreads);
        } else {
            logNull("sendThreads");
        }
    }

    /**
     * Set whether or not Bugsnag should persist user information between application sessions.
     * <p>
     * If enabled then any user information set will be re-used until the user information is
     * removed manually by calling {@link Bugsnag#setUser(String, String, String)}
     * with null arguments.
     *
     * @return boolean
     */
    public boolean getPersistUser() {
        return impl.getPersistUser();
    }

    /**
     * Set whether or not Bugsnag should persist user information between application sessions.
     * <p>
     * If enabled then any user information set will be re-used until the user information is
     * removed manually by calling {@link Bugsnag#setUser(String, String, String)}
     * with null arguments.
     *
     * @param persistUser
     */
    public void setPersistUser(boolean persistUser) {
        impl.setPersistUser(persistUser);
    }

    /**
     * Sets the directory where event and session JSON payloads should be persisted if a network
     * request is not successful. If you use Bugsnag in multiple processes, then a unique
     * persistenceDirectory <b>must</b> be configured for each process to prevent duplicate
     * requests being made by each instantiation of Bugsnag.
     * <p/>
     * The persistenceDirectory also stores user information if {@link #getPersistUser()} has been
     * set to true.
     * <p/>
     * By default, bugsnag sets the persistenceDirectory to {@link Context#getCacheDir()}.
     * <p/>
     * If the persistenceDirectory is changed between application launches, no attempt will be made
     * to deliver events or sessions cached in the previous location.
     *
     * @return file
     */
    @Nullable
    public File getPersistenceDirectory() {
        return impl.getPersistenceDirectory();
    }

    /**
     * Sets the directory where event and session JSON payloads should be persisted if a network
     * request is not successful. If you use Bugsnag in multiple processes, then a unique
     * persistenceDirectory <b>must</b> be configured for each process to prevent duplicate
     * requests being made by each instantiation of Bugsnag.
     * <p/>
     * The persistenceDirectory also stores user information if {@link #getPersistUser()} has been
     * set to true.
     * <p/>
     * By default, bugsnag sets the persistenceDirectory to {@link Context#getCacheDir()}.
     * <p/>
     * If the persistenceDirectory is changed between application launches, no attempt will be made
     * to deliver events or sessions cached in the previous location.
     *
     * @param directory
     */
    public void setPersistenceDirectory(@Nullable File directory) {
        impl.setPersistenceDirectory(directory);
    }

    /**
     * Deprecated. Use {@link #getLaunchDurationMillis()} instead.
     *
     * @return LaunchCrashThresholdMs
     */
    @Deprecated
    public long getLaunchCrashThresholdMs() {
        getLogger().w("The launchCrashThresholdMs configuration option is deprecated "
            + "and will be removed in a future release. Please use "
            + "launchDurationMillis instead.");
        return getLaunchDurationMillis();
    }

    /**
     * Deprecated. Use {@link #setLaunchDurationMillis(long)} instead.
     *
     * @param launchCrashThresholdMs
     */
    @Deprecated
    public void setLaunchCrashThresholdMs(long launchCrashThresholdMs) {
        getLogger().w("The launchCrashThresholdMs configuration option is deprecated "
            + "and will be removed in a future release. Please use "
            + "launchDurationMillis instead.");
        setLaunchDurationMillis(launchCrashThresholdMs);
    }

    /**
     * Sets whether or not Bugsnag should send crashes synchronously that occurred during
     * the application's launch period. By default this behavior is enabled.
     * <p>
     * See {@link #setLaunchDurationMillis(long)}
     *
     * @return boolean
     */
    public boolean getSendLaunchCrashesSynchronously() {
        return impl.getSendLaunchCrashesSynchronously();
    }

    /**
     * Sets whether or not Bugsnag should send crashes synchronously that occurred during
     * the application's launch period. By default this behavior is enabled.
     * <p>
     * See {@link #setLaunchDurationMillis(long)}
     *
     * @param sendLaunchCrashesSynchronously
     */
    public void setSendLaunchCrashesSynchronously(boolean sendLaunchCrashesSynchronously) {
        impl.setSendLaunchCrashesSynchronously(sendLaunchCrashesSynchronously);
    }

    /**
     * Sets the threshold in milliseconds for an uncaught error to be considered as a crash on
     * launch. If a crash is detected on launch, Bugsnag will attempt to send the most recent
     * event synchronously.
     * <p>
     * By default, this value is set at 5,000ms. Setting the value to 0 will count all crashes
     * as launch crashes until markLaunchCompleted() is called.
     *
     * @return LaunchDurationMillis
     */
    public long getLaunchDurationMillis() {
        return impl.getLaunchDurationMillis();
    }

    /**
     * Sets the threshold in milliseconds for an uncaught error to be considered as a crash on
     * launch. If a crash is detected on launch, Bugsnag will attempt to send the most recent
     * event synchronously.
     * <p>
     * By default, this value is set at 5,000ms. Setting the value to 0 will count all crashes
     * as launch crashes until markLaunchCompleted() is called.
     *
     * @param launchDurationMillis
     */
    public void setLaunchDurationMillis(long launchDurationMillis) {
        if (launchDurationMillis >= MIN_LAUNCH_CRASH_THRESHOLD_MS) {
            impl.setLaunchDurationMillis(launchDurationMillis);
        } else {
            getLogger().e("Invalid configuration value detected. "
                + "Option launchDurationMillis should be a positive long value."
                + "Supplied value is " + launchDurationMillis);
        }
    }

    /**
     * Sets whether or not Bugsnag should automatically capture and report User sessions whenever
     * the app enters the foreground.
     * <p>
     * By default this behavior is enabled.
     *
     * @return boolean
     */
    public boolean getAutoTrackSessions() {
        return impl.getAutoTrackSessions();
    }

    /**
     * Sets whether or not Bugsnag should automatically capture and report User sessions whenever
     * the app enters the foreground.
     * <p>
     * By default this behavior is enabled.
     *
     * @param autoTrackSessions
     */
    public void setAutoTrackSessions(boolean autoTrackSessions) {
        impl.setAutoTrackSessions(autoTrackSessions);
    }

    /**
     * Bugsnag will automatically detect different types of error in your application.
     * If you wish to control exactly which types are enabled, set this property.
     *
     * @return ErrorTypes
     */
    @NonNull
    public ErrorTypes getEnabledErrorTypes() {
        return impl.getEnabledErrorTypes();
    }

    /**
     * Bugsnag will automatically detect different types of error in your application.
     * If you wish to control exactly which types are enabled, set this property.
     *
     * @param enabledErrorTypes
     */
    public void setEnabledErrorTypes(@NonNull ErrorTypes enabledErrorTypes) {
        if (enabledErrorTypes != null) {
            impl.setEnabledErrorTypes(enabledErrorTypes);
        } else {
            logNull("enabledErrorTypes");
        }
    }

    /**
     * If you want to disable automatic detection of all errors, you can set this property to false.
     * By default this property is true.
     * <p>
     * Setting autoDetectErrors to false will disable all automatic errors, regardless of the
     * error types enabled by enabledErrorTypes
     *
     * @return boolean
     */
    public boolean getAutoDetectErrors() {
        return impl.getAutoDetectErrors();
    }

    /**
     * If you want to disable automatic detection of all errors, you can set this property to false.
     * By default this property is true.
     * <p>
     * Setting autoDetectErrors to false will disable all automatic errors, regardless of the
     * error types enabled by enabledErrorTypes
     *
     * @param autoDetectErrors
     */
    public void setAutoDetectErrors(boolean autoDetectErrors) {
        impl.setAutoDetectErrors(autoDetectErrors);
    }

    /**
     * If your app's codebase contains different entry-points/processes, but reports to a single
     * Bugsnag project, you might want to add information denoting the type of process the error
     * came from.
     * <p>
     * This information can be used in the dashboard to filter errors and to determine whether
     * an error is limited to a subset of appTypes.
     * <p>
     * By default, this value is set to 'ohos'.
     *
     * @return app type
     */
    @Nullable
    public String getAppType() {
        return impl.getAppType();
    }

    /**
     * If your app's codebase contains different entry-points/processes, but reports to a single
     * Bugsnag project, you might want to add information denoting the type of process the error
     * came from.
     * <p>
     * This information can be used in the dashboard to filter errors and to determine whether
     * an error is limited to a subset of appTypes.
     * <p>
     * By default, this value is set to 'ohos'.
     *
     * @param appType
     */
    public void setAppType(@Nullable String appType) {
        impl.setAppType(appType);
    }

    /**
     * By default, the notifier's log messages will be logged
     * with a "Bugsnag" tag unless the releaseStage is "production".
     * <p>
     * To override this behavior, an alternative instance can be provided that implements the
     * Logger interface.
     *
     * @return Logger
     */
    @Nullable
    public Logger getLogger() {
        return impl.getLogger();
    }

    /**
     * By default, the notifier's log messages will be logged
     * with a "Bugsnag" tag unless the releaseStage is "production".
     * <p>
     * To override this behavior, an alternative instance can be provided that implements the
     * Logger interface.
     *
     * @param logger
     */
    public void setLogger(@Nullable Logger logger) {
        impl.setLogger(logger);
    }

    /**
     * The Delivery implementation used to make network calls to the Bugsnag
     * <a href="https://docs.bugsnag.com/api/error-reporting/">Error Reporting</a> and
     * <a href="https://docs.bugsnag.com/api/sessions/">Sessions API</a>.
     * <p>
     * This may be useful if you have requirements such as certificate pinning and rotation,
     * which are not supported by the default implementation.
     * <p>
     * To provide custom delivery functionality, create a class which implements the Delivery
     * interface. Please note that request bodies must match the structure specified in the
     * <a href="https://docs.bugsnag.com/api/error-reporting/">Error Reporting</a> and
     * <a href="https://docs.bugsnag.com/api/sessions/">Sessions API</a> documentation.
     * <p>
     * You can use the return type from the deliver functions to control the strategy for
     * retrying the transmission at a later date.
     * <p>
     * If DeliveryStatus.UNDELIVERED is returned, the notifier will automatically cache
     * the payload and trigger delivery later on. Otherwise, if either DeliveryStatus.DELIVERED
     * or DeliveryStatus.FAILURE is returned the notifier will removed any cached payload
     * and no further delivery will be attempted.
     *
     * @return Delivery
     */
    @NonNull
    public Delivery getDelivery() {
        return impl.getDelivery();
    }

    /**
     * The Delivery implementation used to make network calls to the Bugsnag
     * <a href="https://docs.bugsnag.com/api/error-reporting/">Error Reporting</a> and
     * <a href="https://docs.bugsnag.com/api/sessions/">Sessions API</a>.
     * <p>
     * This may be useful if you have requirements such as certificate pinning and rotation,
     * which are not supported by the default implementation.
     * <p>
     * To provide custom delivery functionality, create a class which implements the Delivery
     * interface. Please note that request bodies must match the structure specified in the
     * <a href="https://docs.bugsnag.com/api/error-reporting/">Error Reporting</a> and
     * <a href="https://docs.bugsnag.com/api/sessions/">Sessions API</a> documentation.
     * <p>
     * You can use the return type from the deliver functions to control the strategy for
     * retrying the transmission at a later date.
     * <p>
     * If DeliveryStatus.UNDELIVERED is returned, the notifier will automatically cache
     * the payload and trigger delivery later on. Otherwise, if either DeliveryStatus.DELIVERED
     * or DeliveryStatus.FAILURE is returned the notifier will removed any cached payload
     * and no further delivery will be attempted.
     *
     * @param delivery
     */
    public void setDelivery(@NonNull Delivery delivery) {
        if (delivery != null) {
            impl.setDelivery(delivery);
        } else {
            logNull("delivery");
        }
    }

    /**
     * Set the endpoints to send data to. By default we'll send error reports to
     * https://notify.bugsnag.com, and sessions to https://sessions.bugsnag.com, but you can
     * override this if you are using Bugsnag Enterprise to point to your own Bugsnag endpoints.
     *
     * @return EndpointConfiguration
     */
    @NonNull
    public EndpointConfiguration getEndpoints() {
        return impl.getEndpoints();
    }

    /**
     * Set the endpoints to send data to. By default we'll send error reports to
     * https://notify.bugsnag.com, and sessions to https://sessions.bugsnag.com, but you can
     * override this if you are using Bugsnag Enterprise to point to your own Bugsnag endpoints.
     *
     * @param endpoints
     */
    public void setEndpoints(@NonNull EndpointConfiguration endpoints) {
        if (endpoints != null) {
            impl.setEndpoints(endpoints);
        } else {
            logNull("endpoints");
        }
    }

    /**
     * Sets the maximum number of breadcrumbs which will be stored. Once the threshold is reached,
     * the oldest breadcrumbs will be deleted.
     * <p>
     * By default, 25 breadcrumbs are stored: this can be amended up to a maximum of 100.
     *
     * @return MaxBreadcrumbs
     */
    public int getMaxBreadcrumbs() {
        return impl.getMaxBreadcrumbs();
    }

    /**
     * Sets the maximum number of breadcrumbs which will be stored. Once the threshold is reached,
     * the oldest breadcrumbs will be deleted.
     * <p>
     * By default, 25 breadcrumbs are stored: this can be amended up to a maximum of 100.
     *
     * @param maxBreadcrumbs
     */
    public void setMaxBreadcrumbs(int maxBreadcrumbs) {
        if (maxBreadcrumbs >= MIN_BREADCRUMBS && maxBreadcrumbs <= MAX_BREADCRUMBS) {
            impl.setMaxBreadcrumbs(maxBreadcrumbs);
        } else {
            getLogger().e("Invalid configuration value detected. "
                + "Option maxBreadcrumbs should be an integer between 0-100. "
                + "Supplied value is " + maxBreadcrumbs);
        }
    }

    /**
     * Sets the maximum number of persisted events which will be stored. Once the threshold is
     * reached, the oldest event will be deleted.
     * <p>
     * By default, 32 events are persisted.
     *
     * @return MaxPersistedEvents
     */
    public int getMaxPersistedEvents() {
        return impl.getMaxPersistedEvents();
    }

    /**
     * Sets the maximum number of persisted events which will be stored. Once the threshold is
     * reached, the oldest event will be deleted.
     * <p>
     * By default, 32 events are persisted.
     *
     * @param maxPersistedEvents
     */
    public void setMaxPersistedEvents(int maxPersistedEvents) {
        if (maxPersistedEvents >= 0) {
            impl.setMaxPersistedEvents(maxPersistedEvents);
        } else {
            getLogger().e("Invalid configuration value detected. "
                + "Option maxPersistedEvents should be a positive integer."
                + "Supplied value is " + maxPersistedEvents);
        }
    }

    /**
     * Sets the maximum number of persisted sessions which will be stored. Once the threshold is
     * reached, the oldest session will be deleted.
     * <p>
     * By default, 128 sessions are persisted.
     *
     * @return MaxPersistedSessions
     */
    public int getMaxPersistedSessions() {
        return impl.getMaxPersistedSessions();
    }

    /**
     * Sets the maximum number of persisted sessions which will be stored. Once the threshold is
     * reached, the oldest session will be deleted.
     * <p>
     * By default, 128 sessions are persisted.
     *
     * @param maxPersistedSessions
     */
    public void setMaxPersistedSessions(int maxPersistedSessions) {
        if (maxPersistedSessions >= 0) {
            impl.setMaxPersistedSessions(maxPersistedSessions);
        } else {
            getLogger().e("Invalid configuration value detected. "
                + "Option maxPersistedSessions should be a positive integer."
                + "Supplied value is " + maxPersistedSessions);
        }
    }

    /**
     * Bugsnag uses the concept of "contexts" to help display and group your errors. Contexts
     * represent what was happening in your application at the time an error occurs.
     * <p>
     * In an ohos app the "context" is automatically set as the foreground Activity.
     * If you would like to set this value manually, you should alter this property.
     *
     * @return context
     */
    @Nullable
    public String getContext() {
        return impl.getContext();
    }

    /**
     * Bugsnag uses the concept of "contexts" to help display and group your errors. Contexts
     * represent what was happening in your application at the time an error occurs.
     * <p>
     * In an ohos app the "context" is automatically set as the foreground Activity.
     * If you would like to set this value manually, you should alter this property.
     *
     * @param context
     */
    public void setContext(@Nullable String context) {
        impl.setContext(context);
    }

    /**
     * Sets which values should be removed from any Metadata objects before
     * sending them to Bugsnag. Use this if you want to ensure you don't send
     * sensitive data such as passwords, and credit card numbers to our
     * servers. Any keys which contain these strings will be filtered.
     * <p>
     * By default, redactedKeys is set to "password"
     *
     * @return RedactedKeys
     */
    @NonNull
    public Set<String> getRedactedKeys() {
        return impl.getRedactedKeys();
    }

    /**
     * Sets which values should be removed from any Metadata objects before
     * sending them to Bugsnag. Use this if you want to ensure you don't send
     * sensitive data such as passwords, and credit card numbers to our
     * servers. Any keys which contain these strings will be filtered.
     * <p>
     * By default, redactedKeys is set to "password"
     *
     * @param redactedKeys
     */
    public void setRedactedKeys(@NonNull Set<String> redactedKeys) {
        if (CollectionUtils.containsNullElements(redactedKeys)) {
            logNull("redactedKeys");
        } else {
            impl.setRedactedKeys(redactedKeys);
        }
    }

    /**
     * Allows you to specify the fully-qualified name of error classes that will be discarded
     * before being sent to Bugsnag if they are detected. The notifier performs an exact
     * match against the canonical class name.
     *
     * @return DiscardClasses
     */
    @NonNull
    public Set<String> getDiscardClasses() {
        return impl.getDiscardClasses();
    }

    /**
     * Allows you to specify the fully-qualified name of error classes that will be discarded
     * before being sent to Bugsnag if they are detected. The notifier performs an exact
     * match against the canonical class name.
     *
     * @param discardClasses
     */
    public void setDiscardClasses(@NonNull Set<String> discardClasses) {
        if (CollectionUtils.containsNullElements(discardClasses)) {
            logNull("discardClasses");
        } else {
            impl.setDiscardClasses(discardClasses);
        }
    }

    /**
     * By default, Bugsnag will be notified of events that happen in any releaseStage.
     * If you would like to change which release stages notify Bugsnag you can set this property.
     *
     * @return EnabledReleaseStages
     */
    @Nullable
    public Set<String> getEnabledReleaseStages() {
        return impl.getEnabledReleaseStages();
    }

    /**
     * By default, Bugsnag will be notified of events that happen in any releaseStage.
     * If you would like to change which release stages notify Bugsnag you can set this property.
     *
     * @param enabledReleaseStages
     */
    public void setEnabledReleaseStages(@Nullable Set<String> enabledReleaseStages) {
        impl.setEnabledReleaseStages(enabledReleaseStages);
    }

    /**
     * By default we will automatically add breadcrumbs for common application events such as
     * activity lifecycle events and system intents. To amend this behavior,
     * override the enabled breadcrumb types. All breadcrumbs can be disabled by providing an
     * empty set.
     * <p>
     * The following breadcrumb types can be enabled:
     * <p>
     * - Captured errors: left when an error event is sent to the Bugsnag API.
     * - Manual breadcrumbs: left via the Bugsnag.leaveBreadcrumb function.
     * - Navigation changes: left for Activity Lifecycle events to track the user's journey in
     * the app.
     * - State changes: state breadcrumbs are left for system broadcast events. For example:
     * battery warnings, airplane mode, etc.
     * - User interaction: left when the user performs certain system operations.
     *
     * @return EnabledBreadcrumbTypes
     */
    @Nullable
    public Set<BreadcrumbType> getEnabledBreadcrumbTypes() {
        return impl.getEnabledBreadcrumbTypes();
    }

    /**
     * By default we will automatically add breadcrumbs for common application events such as
     * activity lifecycle events and system intents. To amend this behavior,
     * override the enabled breadcrumb types. All breadcrumbs can be disabled by providing an
     * empty set.
     * <p>
     * The following breadcrumb types can be enabled:
     * <p>
     * - Captured errors: left when an error event is sent to the Bugsnag API.
     * - Manual breadcrumbs: left via the Bugsnag.leaveBreadcrumb function.
     * - Navigation changes: left for Activity Lifecycle events to track the user's journey in
     * the app.
     * - State changes: state breadcrumbs are left for system broadcast events. For example:
     * battery warnings, airplane mode, etc.
     * - User interaction: left when the user performs certain system operations.
     *
     * @param enabledBreadcrumbTypes
     */
    public void setEnabledBreadcrumbTypes(@Nullable Set<BreadcrumbType> enabledBreadcrumbTypes) {
        impl.setEnabledBreadcrumbTypes(enabledBreadcrumbTypes);
    }

    /**
     * Sets which package names Bugsnag should consider as a part of the
     * running application. We mark stacktrace lines as in-project if they
     * originate from any of these packages and this allows us to improve
     * the visual display of the stacktrace on the dashboard.
     * <p>
     * By default, projectPackages is set to be the package you called Bugsnag.start from.
     *
     * @return ProjectPackages
     */
    @NonNull
    public Set<String> getProjectPackages() {
        return impl.getProjectPackages();
    }

    /**
     * Sets which package names Bugsnag should consider as a part of the
     * running application. We mark stacktrace lines as in-project if they
     * originate from any of these packages and this allows us to improve
     * the visual display of the stacktrace on the dashboard.
     * <p>
     * By default, projectPackages is set to be the package you called Bugsnag.start from.
     *
     * @param projectPackages
     */
    public void setProjectPackages(@NonNull Set<String> projectPackages) {
        if (CollectionUtils.containsNullElements(projectPackages)) {
            logNull("projectPackages");
        } else {
            impl.setProjectPackages(projectPackages);
        }
    }

    /**
     * Add a "on error" callback, to execute code at the point where an error report is
     * captured in Bugsnag.
     * <p>
     * You can use this to add or modify information attached to an Event
     * before it is sent to your dashboard. You can also return
     * <code>false</code> from any callback to prevent delivery. "on error"
     * callbacks do not run before reports generated in the event
     * of immediate app termination from crashes in C/C++ code.
     * <p>
     * For example:
     * <p>
     * Bugsnag.addOnError(new OnErrorCallback() {
     * public boolean run(Event event) {
     * event.setSeverity(Severity.INFO);
     * return true;
     * }
     * })
     *
     * @param onError a callback to run before sending errors to Bugsnag
     * @see OnErrorCallback
     */
    @Override
    public void addOnError(@NonNull OnErrorCallback onError) {
        if (onError != null) {
            impl.addOnError(onError);
        } else {
            logNull("addOnError");
        }
    }

    /**
     * Removes a previously added "on error" callback
     *
     * @param onError the callback to remove
     */
    @Override
    public void removeOnError(@NonNull OnErrorCallback onError) {
        if (onError != null) {
            impl.removeOnError(onError);
        } else {
            logNull("removeOnError");
        }
    }

    /**
     * Add an "on breadcrumb" callback, to execute code before every
     * breadcrumb captured by Bugsnag.
     * <p>
     * You can use this to modify breadcrumbs before they are stored by Bugsnag.
     * You can also return <code>false</code> from any callback to ignore a breadcrumb.
     * <p>
     * For example:
     * <p>
     * Bugsnag.onBreadcrumb(new OnBreadcrumbCallback() {
     * public boolean run(Breadcrumb breadcrumb) {
     * return false; // ignore the breadcrumb
     * }
     * })
     *
     * @param onBreadcrumb a callback to run before a breadcrumb is captured
     * @see OnBreadcrumbCallback
     */
    @Override
    public void addOnBreadcrumb(@NonNull OnBreadcrumbCallback onBreadcrumb) {
        if (onBreadcrumb != null) {
            impl.addOnBreadcrumb(onBreadcrumb);
        } else {
            logNull("addOnBreadcrumb");
        }
    }

    /**
     * Removes a previously added "on breadcrumb" callback
     *
     * @param onBreadcrumb the callback to remove
     */
    @Override
    public void removeOnBreadcrumb(@NonNull OnBreadcrumbCallback onBreadcrumb) {
        if (onBreadcrumb != null) {
            impl.removeOnBreadcrumb(onBreadcrumb);
        } else {
            logNull("removeOnBreadcrumb");
        }
    }

    /**
     * Add an "on session" callback, to execute code before every
     * session captured by Bugsnag.
     * <p>
     * You can use this to modify sessions before they are stored by Bugsnag.
     * You can also return <code>false</code> from any callback to ignore a session.
     * <p>
     * For example:
     * <p>
     * Bugsnag.onSession(new OnSessionCallback() {
     * public boolean run(Session session) {
     * return false; // ignore the session
     * }
     * })
     *
     * @param onSession a callback to run before a session is captured
     * @see OnSessionCallback
     */
    @Override
    public void addOnSession(@NonNull OnSessionCallback onSession) {
        if (onSession != null) {
            impl.addOnSession(onSession);
        } else {
            logNull("addOnSession");
        }
    }

    /**
     * Removes a previously added "on session" callback
     *
     * @param onSession the callback to remove
     */
    @Override
    public void removeOnSession(@NonNull OnSessionCallback onSession) {
        if (onSession != null) {
            impl.removeOnSession(onSession);
        } else {
            logNull("removeOnSession");
        }
    }

    /**
     * Adds a map of multiple metadata key-value pairs to the specified section.
     *
     * @param section
     * @param value
     */
    @Override
    public void addMetadata(@NonNull String section, @NonNull Map<String, Object> value) {
        if (section != null && value != null) {
            impl.addMetadata(section, value);
        } else {
            logNull("addMetadata");
        }
    }

    /**
     * Adds the specified key and value in the specified section. The value can be of
     * any primitive type or a collection such as a map, set or array.
     *
     * @param section
     * @param key
     * @param value
     */
    @Override
    public void addMetadata(@NonNull String section, @NonNull String key, @Nullable Object value) {
        if (section != null && key != null) {
            impl.addMetadata(section, key, value);
        } else {
            logNull("addMetadata");
        }
    }

    /**
     * Removes all the data from the specified section.
     *
     * @param section
     */
    @Override
    public void clearMetadata(@NonNull String section) {
        if (section != null) {
            impl.clearMetadata(section);
        } else {
            logNull("clearMetadata");
        }
    }

    /**
     * Removes data with the specified key from the specified section.
     *
     * @param section
     * @param key
     */
    @Override
    public void clearMetadata(@NonNull String section, @NonNull String key) {
        if (section != null && key != null) {
            impl.clearMetadata(section, key);
        } else {
            logNull("clearMetadata");
        }
    }

    /**
     * Returns a map of data in the specified section.
     *
     * @param section
     */
    @Nullable
    @Override
    public Map<String, Object> getMetadata(@NonNull String section) {
        if (section != null) {
            return impl.getMetadata(section);
        } else {
            logNull("getMetadata");
            return null;
        }
    }

    /**
     * Returns the value of the specified key in the specified section.
     *
     * @return meta data
     */
    @Nullable
    @Override
    public Object getMetadata(@NonNull String section, @NonNull String key) {
        if (section != null && key != null) {
            return impl.getMetadata(section, key);
        } else {
            logNull("getMetadata");
            return null;
        }
    }

    /**
     * Returns the currently set User information.
     *
     * @return user
     */
    @NonNull
    @Override
    public User getUser() {
        return impl.getUser();
    }

    /**
     * Sets the user associated with the event.
     *
     * @param id
     * @param email
     * @param name
     */
    @Override
    public void setUser(@Nullable String id, @Nullable String email, @Nullable String name) {
        impl.setUser(id, email, name);
    }

    /**
     * Adds a plugin which will be loaded when the bugsnag notifier is instantiated.
     *
     * @param plugin
     */
    public void addPlugin(@NonNull Plugin plugin) {
        if (plugin != null) {
            impl.addPlugin(plugin);
        } else {
            logNull("addPlugin");
        }
    }

    Set<Plugin> getPlugins() {
        return impl.getPlugins();
    }
}
